package core;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.parsers.ParserConfigurationException;
import javax.swing.text.BadLocationException;

import core.Blog.Update_type;
import events.PostStateChangedEvent;
import events.PostStateChangedListener;

@XmlRootElement
public class Post implements Serializable {
	// Class's constants.
	private final static long serialVersionUID = 1;
	private static final String IMG_DIRNAME = "img/";
	private static final String POST_EXT = "html";
	private static final String ZIP_EXT = "zip";
	private static final String NOTITLE_MSG = "Post with no title";
	
	// Class's attributes.
	private Date date = null;
	private String title = null;
	private String author = null;	
	private String content = null;
	private URL url = null;
	private transient HashMap<String, Img>Images = null;
	private boolean already_read = false;
	private Update_type update_type = Update_type.NO_UPDATE;
	private Post_state state = Post_state.UNKNOWN;
	private transient List<PostStateChangedListener> psc_listeners = null;
		
	// The possible states of a Post.
	public enum Post_state {
		UNKNOWN,
		ACTUAL,
		ARCHIVED
	}
	
	/*
	 * Private methods.
	 */
	
	/**
	 * Method that modify the content of the post changing the value of the src
	 * attribute of its img tags from src_to_search to src_to_replace.
	 * 
	 * @param src_to_search
	 * 		The value to search for inside the img tags src attribute.
	 * @param src_to_replace
	 * 		The value to replace with for all the src_to_search founded.
	 * @throws BadLocationException 
	 * @throws IOException 
	 * @throws RuntimeException
	 * 		If the images weren't loaded before calling this method.
	 */
	private void modifySrcAttrOfImgTags(String src_to_search,
										String src_to_replace)
	throws IOException, BadLocationException {
		String imgtag_regex, replacement_str;
		
		// Check if the images haven't been loaded.
		if (this.Images == null)
			throw new RuntimeException("Images haven't been loaded.");
		
		// Build a properly regular expression and also a proper replacement
		// string to do all the magic.
		imgtag_regex = "(<img[^>]+src\\s*=\\s*[\"'])";
		imgtag_regex += Util.escapeRegExChars(src_to_search.toCharArray());
		imgtag_regex += "([\"'][^>]*>)";
		replacement_str = "$1" +
						  Util.escapeRegExChars(src_to_replace.toCharArray()) +
						  "$2";
		content = content.replaceAll(imgtag_regex, replacement_str);
	}
	
	/*
	 * Class's methods.
	 */
	
	/**
	 * Empty constructor, needed to serialize.
	 */
	public Post() {}
	
	/**
	 * Post main constructor.
	 * 
	 * @param
	 * 		Post identifier. Note that here we cannot control if this value is
	 * 		being used as a unique identifier. This has to be properly handled
	 * 		outside of this class.
	 * @param date
	 * 		Release date of the post.
	 * @param title
	 * 		Title of the post.
	 * @param url
	 * 		URL of the post where you can watch it.
	 * @param content
	 * 		Content of the post.
	 */
	public Post(Date date, String title, URL url,
				String author, String content) {
		this.date = date;
		this.title = title != null? title: NOTITLE_MSG; 
		this.url = url;
		this.author = author;
		this.content = content;
		this.already_read = false;
		this.psc_listeners = new ArrayList<PostStateChangedListener>();
	}
	
	/**
	 * Adds a handler for when this Post changes its state.
	 * 
	 * @param handler:
	 * 		A PostStateChangedListener instance of that interface.
	 */
	public void addPostStateChangedListener(PostStateChangedListener handler) {
		if (psc_listeners == null)
			psc_listeners = new ArrayList<PostStateChangedListener>();
		psc_listeners.add(handler);
	}
	
	/**
	 * This method modifies the content of the Post, cleaning every src
	 * attribute of every img tag.
	 * 
	 * This is an intrusive method.
	 * 
	 * @return:
	 * 		The content of the Post after the cleaning.
	 */
	public String cleanSrcs() {
		// Regular expression that searchs img tags and groups everything
		// before its src value and everything after its src value.
		String regex = "(<img [^>]*src\\s*=\\s*['\"])[^'\"](['\"][^>]*>)";
		String repl_str = "$1$2";
		content = content.replaceAll(regex, repl_str);
		return content;
	}
	
	/**
	 * Big bad magic, this function is hard to understand but it makes a lot
	 * of work. What it makes is basically create a zip file that contains a
	 * html file with the post content and a directory (named "img/") that
	 * contains all the images the post references to. The content of the html
	 * is not the original one, it is modified on-the-fly to make img tags
	 * point to the appropriate image inside the "img/" directory.
	 * 
	 * This method is intrusive, it changes the content of the post.
	 * 
	 * Here is how we do it:
	 * 	- The temporary directory is created in order to make easy the cleaning
	 * 	  at the end.
	 * 	- The directory for the images is created.
	 * 	- The images are downloaded and saved inside the images directory.
	 * 	- The html content of the post is modified to make img tags point at
	 * 	  the local images.
	 * 	- The content of the post is written to a file.
	 * 	- All the resources previously created (images directory containing
	 * 	  images and the html file with the post's content) are zipped to a
	 * 	  single zip file.
	 * 	- The temporary directory created at the beginning is removed.
	 *
	 * @param path
	 * 		The path to where put the zip file.
	 * @param zipname
	 * 		The name that the zip file will has (without the .zip extension).
	 * @throws IOException 
	 * @throws NoSuchAlgorithmException 
	 * @throws BadLocationException 
	 * @throws SAXException 
	 * @throws ParserConfigurationException 
	 */
	public String createZip(String path, String zipname) throws IOException,
	NoSuchAlgorithmException, BadLocationException {
		String checked_path = Util.checkDirectoryPath(path);
		String post_dir;
		String images_dir, abs_img_name, rel_img_name, img_fname;
		String src, post_fname, complete_zipname;
		File post_fd, images_fd;
		List<Img> images_list;
		
		// Creating the temporary directory.
		post_dir = checked_path + zipname;
		post_fd = new File(post_dir);
		if (post_fd.exists())
			throw new RuntimeException("Folder " + post_dir +
									   " already exists.");
		post_fd.mkdir();
		
		// Creates the directory for the images.
		images_dir = Util.checkDirectoryPath(post_fd.getAbsolutePath()) +
					 IMG_DIRNAME;
		images_fd = new File(images_dir);
		if (images_fd.exists())
			throw new RuntimeException("Folder " + images_fd.getAbsolutePath()
									   + " already exists.");
		images_fd.mkdir();
		
		loadImages();
		images_list = getImages();
		for (Img image: images_list) {
			image.getImage();
			img_fname = "image" + images_list.indexOf(image) +
						Util.EXT_SEPARATOR + image.getType();
			
			// Save the image inside its directory..
			abs_img_name = Util.checkDirectoryPath(images_fd.getAbsolutePath())
						   + img_fname;
			image.save2File(abs_img_name);
			
			// Modify the content to make the img tags src attribute to point
			// at local.
			src = image.getUrl().toString();
			rel_img_name = Util.checkDirectoryPath(images_fd.getName()) +
						   img_fname;
			modifySrcAttrOfImgTags(src, rel_img_name);
		}
		
		// Save the html of the post to a file.
		post_fname = Util.checkDirectoryPath(post_fd.getAbsolutePath()) +
					 getTitle() + Util.EXT_SEPARATOR + POST_EXT;
		save2File(post_fname);
		
		// Zip the temporary directory.
		complete_zipname = checked_path + zipname +
						   Util.EXT_SEPARATOR + ZIP_EXT;
		Util.zipDir(post_dir, complete_zipname);
		
		// Erase the temporary directory.
		if (!Util.rmdirInDepth(post_dir))
			throw new RuntimeException("Cannot delete directory " + post_dir);
		
		return complete_zipname;
	}
	
	/**
	 * Returns the author of the post.
	 * 
	 * @return
	 * 		A String representing the author of the post.
	 */
	public String getAuthor() {
		return author;
	}
	
	/**
	 * Returns the XML content of the post.
	 * 
	 * @return
	 * 		A String with the XML content of the post.
	 */
	public String getContent() {
		return content;
	}	
	
	/**
	 * Returns the release date of the post.
	 * 
	 * @return
	 * 		A Date object containing the release date.
	 */
	public Date getDate() {
		return date;
	}
	
	/**
	 * Returns a list with all the images used in the post. This method
	 * should be called just after call the loadImages() method.
	 * 
	 * @return
	 * 		A list with the Img objects representing the images used in this
	 * 		post or null if loadImages() hasn't been called.
	 */
	public List<Img> getImages(){
		List<Img> res;
		
		if (Images == null)
			throw new RuntimeException("Images haven't been loaded.");
		
		res = new ArrayList<Img>();
		for (Img image: Images.values())
			res.add(image);
		
		return res;
	}
	
	/**
	 * Returns the Post_state of this Post.
	 * 
	 * @return:
	 * 		The Post_state value associated to this Post.
	 */
	public Post_state getState() {
		return state;
	}
	
	/**
	 * Returns the title of the post.
	 * 
	 * @return:
	 * 		A String representing the title of the post.
	 */
	public String getTitle() {
		return title;
	}
	
	/**
	 * Returns the update type of this post.
	 * 
	 * @return:
	 * 		The update type of this post.
	 */
	public Update_type getUpdateType() {
		return update_type;
	}
	
	/**
	 * Returns the URL where the post can be found.
	 * 
	 * @return
	 * 		An URL object representing the url of the post.
	 */
	public URL getUrl() {
		return url;
	}
	
	/**
	 * Returns true if the post has already been read or false if not.
	 * Note that a post will not be marked as read until method
	 * markAsReaded() has been invoked.
	 * 
	 * @return
	 * 		True if the post has been marked as read, false elsewhere.
	 */
	public boolean hasBeenRead() {
		return already_read;
	}
	
	/**
	 * Check if the Post is considered actual. To know what that means read the
	 * documentation of the Blog.getActualPosts() method.
	 * 
	 * @return:
	 * 		True if the Post is marked as actual, false elsewhere.
	 */
	public boolean isActual() {
		if (state == Post_state.ACTUAL)
			return true;
		return false;
	}
	
	/**
	 * Check if the Post is considered archived. To know what that means read
	 * the documentation of the Blog.getActualPosts() method.
	 * 
	 * @return:
	 * 		True if the Post is marked as archived, false elsewhere.
	 */
	public boolean isArchived() {
		if (state == Post_state.ARCHIVED)
			return true;
		return false;
	}
	
	/**
	 * Constructing a post doesn't load the images data, in order to do it
	 * this method must be called. It will create a list with the images used
	 * in this post. If the images were already loaded it will returns.
	 * 
	 * @return
	 * 		The number of images successfully loaded.
	 * @throws NoSuchAlgorithmException
	 * 		Should never be thrown, needed since Img constructor could also
	 * 		throws it.
	 * @throws IOException 
	 */
	public int loadImages() throws NoSuchAlgorithmException, IOException {
		// Note that this regex has a capturing group (the match between the
		// parenthesis).
		String imgtag_regex = "<img [^>]*src=[\"']([^\"']*)[\"'][^>]*>";
		String http_prefix = "http://";
		String https_prefix = "https://";
		/** TODO: This regex should be revised to determine how detect that
		 * the path is a local reference IN ANY SYSTEM. For example, now it
		 * doesn't detect a reference through the local network in a workgroup
		 * on Windows systems, those that start with \\<ip>\path\to\file. **/
		String local_prefix = "^(\\/.*|[a-zA-Z]:.*)";
		String img_locator, imgtag_content;
		Pattern imgtag_pat;
		Matcher imgtag_mat;
		Img image;
		
		// Search for each img tag.
		imgtag_pat = Pattern.compile(imgtag_regex);
		imgtag_mat = imgtag_pat.matcher(content);
		
		// Check if we have already loaded the images.
		if (Images != null)
			return Images.size();
		else
			Images = new HashMap<String, Img>();
		
		// For each img tag we take the src attribute's value.
		while (imgtag_mat.find()) {
			imgtag_content = imgtag_mat.group(0);
			img_locator = imgtag_mat.group(1);
			
			// If the src location is at the Internet (starts with http or
			// https) build the image from an URL.
			if (img_locator.startsWith(http_prefix) ||
				img_locator.startsWith(https_prefix))
				image = new Img(new URL(img_locator));
			// If the src location is inside our file system build the image
			// from a filename.
			else if (img_locator.matches(local_prefix))
				image = new Img(img_locator);
			else
				throw new RuntimeException("The src value of an image was " +
										   "bad-formed: " + img_locator);
			
			// We build the image and relate it to its img tag. It could occur
			// that the image couldn't be loaded. This can occur for example
			// when the resource at the Internet is no longer available, if we
			// don't have Internet connection or if the image has been built
			// with a filename, if the file no longer exists.
			try {
				image.getImage();
				Images.put(imgtag_content, image);
			}
			catch (IOException e) {}
		}
		
		return this.Images.size();
	}
	
	/**
	 * Modifies (or adds) an "alt" attribute inside each img tag of this post
	 * and adds them the hash of that image in each P2P network. This value
	 * has the form "<P2Pnetwork>:<hash>" separated with commas.
	 * 
	 * Before calling this method, the method loadImages() should was called.
	 * 
	 * This is an intrusive method.
	 * 
	 * @return:
	 * 		A String representing the new content.
	 */
	public String mangleAlts() {
		String imghash, new_imgtag, altattr_val, imgtag_regex;
		HashMap<String, Img> new_Images = new HashMap<String, Img>();
		Img image;
		
		if (Images == null)
			throw new RuntimeException("Images hadn't been loaded.");
		
		// For each imgtag founded while loading images of this post we search
		// for it inside the content of the post, then we create a new img tag
		// with the alt attribute value modified (or a new alt attribute if it
		// haven't it) containing the hashes for each P2P network. Also we
		// build a new HashMap with the images and the new tags modified.
		for (String imgtag: Images.keySet()) {
			// Get the image hash related to this img tag.
			image = Images.get(imgtag);
			imghash = image.getHash();
			
			// Build the new img tag and modify it in the content of the post.
			altattr_val = "GNUTELLA:" + imghash;
			new_imgtag = Util.addAltAttrWithValue(imgtag, altattr_val);
			imgtag_regex = Util.escapeRegExChars(imgtag.toCharArray());
			content = content.replaceAll(imgtag_regex, new_imgtag);
			
			// We build a new HashMap instead modifying the existing one
			// because we are iterating over it and modifying it on-the-fly
			// will occurs in an undefined behaviour.
			new_Images.put(new_imgtag, image);
		}
		
		// Modify the existing images list with the new one.
		Images = new_Images;
		return content;
	}
	
	/**
	 * Set this post as read.
	 */
	public void markAsRead() {
		already_read = true;
	}
	
	/**
	 * Method that modify the src attributes of all the images whose alt
	 * attributes contain the hash passed in the content of this Post.
	 * 
	 * This is an intrusive method.
	 * 
	 * @param alt_to_search:
	 * 		The hash that is spected to be contained in the alt attributes
	 * 		of the img tags to be modified.
	 * @param new_src:
	 * 		The value of the src attributes of the img tags that will be
	 * 		modified.
	 */
	public synchronized void modifyImgSrcFromAlt(String alt_to_search,
												 String new_src) {
		String regex, regex2, imgtag, imgtag_mod, repl_str, mod_cont;
		Pattern pat, pat2;
		Matcher mat, mat2;
		
		// In order to speedup this method we first check that the alt we are
		// searching for exists inside the content, if so we will check
		// later that it exists where it must exist.
		if (!content.contains(alt_to_search))
			return;
		
		// Sorry for this, i know it seems horrible but is the easiest way to
		// check almost all the conditions that must occur to get the src of
		// img tags that contains in its alt the hash searched.
		//
		// BEWARE!: This regex despite being maybe a bit complex, it is quite
		// far from perfection. For example, the quotes surrounding the value
		// of the src and alt could be single or double IN ANY COMBINATION,
		// this means that something like src="url' will be valid... This
		// means that if someone has the great idea of build a feed with src
		// images like this one src="http://domain/file?param=value' or '1'='1"
		// (who will wrote something like that? :) this will fail... who cares?
		// xD.
		// BEWARE!: This regex just works with encoding without CDATA. The RSS
		// specification let the content to be enconded using CDATA sections.
		// Sorry :(
		regex = "<img[^>]*?( src\\s*=\\s*[\"']((?!" +
				Util.escapeRegExChars(new_src.toCharArray()) +
				")[^\"'])*[\"'][^>]*? alt\\s*=\\s*[\"']GNUTELLA:" +
				Util.escapeRegExChars(alt_to_search.toCharArray()) +
				"[\"']| alt\\s*=\\s*[\"']GNUTELLA:" +
				Util.escapeRegExChars(alt_to_search.toCharArray()) +
				"[\"'][^>]*? src\\s*=\\s*[\"']((?!" +
				Util.escapeRegExChars(new_src.toCharArray()) +
				")[^\"'])*[\"'])[^>]*?>";
		
		// Search inside the content of this Post the previous regex.
		mod_cont = content;
		pat = Pattern.compile(regex);
		mat = pat.matcher(mod_cont);
		while (mat.find()) {
			
			// Take the whole matching group. Search inside it just the src
			// attribute and build the replacement string from it.
			//
			// BEWARE!: Sorry about all these BEWARE but i cannot do all the
			// checks (deadlines). The replacement string building have
			// problems if new_src contains '\' or '$'. Check
			// Matcher.replaceAll() documentation.
			imgtag = mat.group(0);
			regex2 = "^(.* src\\s*=\\s*[\"'])[^\"']*([\"'].*)$";
			pat2 = Pattern.compile(regex2);
			repl_str = "$1" +
					   Util.escapeRegExChars(new_src.toCharArray()) +
					   "$2";
			
			// Build a new img tag based on the previous one (with the src
			// modified) and replace it in the content.
			imgtag_mod = imgtag.replaceAll(regex2, repl_str);
			imgtag_mod = Util.escapeRegExChars(imgtag_mod.toCharArray());
			pat2 = Pattern.compile(Util.escapeRegExChars(imgtag.toCharArray()));
			mat2 = pat2.matcher(mod_cont);
			mod_cont = mat2.replaceAll(imgtag_mod);
			
			// Next searches have to be done inside the new modified content in
			// order to avoid find the same reference over and over.
			mat = pat.matcher(mod_cont);
		}
		
		// We have change all the references, update the content.
		content = mod_cont;
	}
	
	/**
	 * Method that saves the HTML content of the post to a file.
	 * 
	 * @param fname
	 * 		The file name where the post must be saved.
	 * @throws IOException
	 */
	public void save2File(String fname) throws IOException {
		FileOutputStream fos;
		
		fos = new FileOutputStream(fname);
		fos.write(this.content.getBytes());
	}
	
	/**
	 * Sets the Post as actual.
	 */
	public void setActual() {
		PostStateChangedEvent event;
		
		// Check that it is a real change.
		if (state != Post_state.ACTUAL) {
			event = new PostStateChangedEvent(this, this.state);
			state = Post_state.ACTUAL;
			for (PostStateChangedListener handler: psc_listeners)
				handler.handleEvent(event);
		}
	}
	
	/**
	 * Set the Post as archived.
	 */
	public void setArchived() {
		PostStateChangedEvent event;
		
		// Check that it is a real change.
		if (state != Post_state.ARCHIVED) {
			event = new PostStateChangedEvent(this, this.state);
			state = Post_state.ARCHIVED;
			for (PostStateChangedListener handler: psc_listeners)
				handler.handleEvent(event);
		}
	}
	
	/**
	 * Sets the title of this post.
	 * 
	 * @param title:
	 * 		A String representing the new title.
	 */
	public void setTitle(String title) {
		this.title = title;
	}
	
	/**
	 * Sets the type of update of this post.
	 * 
	 * @param type:
	 * 		The type of the update of this post.
	 */
	public void setUpdateType(Update_type update_type) {
		this.update_type = update_type;
	}
}